package jdesktop.appbar.win32;

import com.sun.jna.Pointer;
import jdesktop.appbar.AppBar;
import jdesktop.appbar.win32.DragGuide;

import java.awt.*;
import java.util.logging.Level;
import java.util.logging.Logger;

import static jdesktop.appbar.win32.Shell32.*;

/**
 * The common window procedure for docked and floating appbars.
 */
public abstract class CommonWndProc implements User32.WndProc
{
    protected final Logger log = Logger.getLogger(getClass().getName());

    protected static final User32 user32 = User32.INSTANCE;

    protected final WindowMessageHandler messageHandler;
    protected final AppBar      owner;

    protected final RECT    scratchRect = new RECT();
    protected final User32.Point   scratchPoint = new User32.Point();

    protected final WndProcState windowProcedureState;

    /**
     * When {@code true} resizing is restricted to the inner edge adjacent to the desktop.
     * The outer edges are not sizable. This is typically the behaviour required of appbars.
     * Disabling this is used mostly for testing. Even when this is set to {@code false} so
     * that all edges can be sized, the appBar should still "snap back" to an appropriate position.}
     */
    private boolean restrictResize = true;
    

    /**
     * Controls the handling handling of the WM_SIZING notification.
     */
    private boolean handleSizing = true;

    /**
     * Enables handling of the WM_MOVING notification.
     */
    private boolean handleMoving = true;


    private boolean messageHandled = false;
    private int messageResult = 0;
    /**
     * Controls fixing of the hittest code from the default window procedure.
     * @see #fixupSmallWindowHitTestBug
     * @see #handleHitTest
     */
    private boolean applyHitTestBugfix = true;

    /**
     * The shared state that is used by different instances of {@link CommonWndProc}.
     * @param state The shared state.
     */
    public CommonWndProc(WndProcState state)
    {
        this.windowProcedureState = state;
        this.messageHandler = state.getWindowMessageHandler();
        this.owner = state.getAppBar();
    }

    /**
     * Sets the result of hanlding a window message. Used by message handlers to set the
     * message result. If the message is not handled, the callbac procedure will delegate to
     * the original window procedure.
     * @param result    The message result.
     * @see #callback(com.sun.jna.Pointer, int, int, int)
     */
    protected void setMessageResult(int result)
    {
        messageHandled = true;
        messageResult = result;
    }

    public int getMessageResult()
    {
        return messageResult;
    }

    protected void clearMessageResult()
    {
        messageHandled = false;
    }

    protected boolean hasMessageResult()
    {
        return messageHandled;
    }


    /**
     * Provides common handling of methods. Delegates handlig to {@link #handleMessage(com.sun.jna.Pointer, int, int, int)}.
     * If no message result has been set via {@link #setMessageResult(int)}, delegates to the
     * original window procedure, vai {@link WndProcState#callClassWndProc(com.sun.jna.Pointer, int, int, int)}.
     *
     * @param hWnd      The window the message applies to.
     * @param uMsg      The message identifier, see {@code WM_xxx} in {@link User32}, e.g. {@link User32#WM_ACTIVATE}.
     * @param wParam    A pessage parameter - meaning is message-dependent.
     * @param lParam    A message parameter - meaning is message-dependent.
     * @return  A message-dependent return code.
     *
     */
    public int callback(Pointer hWnd, int uMsg, int wParam, int lParam)
    {
        clearMessageResult();
        handleMessage(hWnd, uMsg, wParam, lParam);
        return handleResult(hWnd, uMsg, wParam, lParam);
    }

    /**
     * Determines the result of message handling. If the message was handled,
     * the result provided by the message handler is returned. If the message was
     * not handled, the original window procedure is invoked.
     * @return The result of message handling.
     */
    protected int handleResult(Pointer hWnd, int uMsg, int wParam, int lParam)
    {
        return messageHandled ? messageResult :
            windowProcedureState.callClassWndProc(hWnd, uMsg, wParam, lParam);
    }

    /**
     * Handles the window message by delegating to a specific method for each message.
     * @param hWnd      The window the message applies to.
     * @param uMsg      The message identifier, see {@code WM_xxx} in {@link User32}, e.g. {@link User32#WM_ACTIVATE}.
     * @param wParam    A pessage parameter - meaning is message-dependent.
     * @param lParam    A message parameter - meaning is message-dependent.
     * @return  A message-dependent return code.
     * <p>
     * These messages are handled:
     * <table>
     *  <thead><th>Message</th><th>message handler</th></thead>
     * <tbody>
     *  <tr><td>{@link User32#WM_ENTERSIZEMOVE WM_ENTERSIZEMOVE}</td><td>{@link #handleEnterSizeOrMove()}</td></tr>
     *  <tr><td>{@link User32#WM_EXITSIZEMOVE WM_EXITSIZEMOVE}</td><td>{@link #handleExitSizeOrMove()}</td></tr>
     *  <tr><td>{@link User32#WM_MOVING WM_MOVING}</td><td>{@link #handleMoving(com.sun.jna.Pointer, int, int, int)}</td></tr>
     *  <tr><td>{@link User32#WM_SIZING WM_SIZING}</td><td>{@link #handleSizing(com.sun.jna.Pointer, int, int, int)}</td></tr>
     *  <tr><td>{@link User32#WM_NCHITTEST WM_NCHITTEST}</td><td>{@link #handleHitTest(com.sun.jna.Pointer, int, int, int)}</td></tr>
     * </tbody>
     * </table>
     * </p>
     * <p>
     * For the java component to paint correctly after being resized, the
     * WM_ENTERSIZEMOVE/WM_EXITSIZMOVE must be passed to the subclass. We clear
     * any message result to ensure the subclass wndproc is properly notified.
     * the WM_SIZING message must be passed to the subclass for painting to
     * occur during resizing.
     */
    protected void handleMessage(Pointer hWnd, int uMsg, int wParam, int lParam)
    {
        switch (uMsg)
        {
            case User32.WM_ENTERSIZEMOVE:
                handleEnterSizeOrMove();
                clearMessageResult(); // see paint comment above.
                break;

            case User32.WM_EXITSIZEMOVE:
                handleExitSizeOrMove();
                clearMessageResult(); // see paint comment above.
                break;

            case User32.WM_MOVING:
                handleMoving(hWnd, uMsg, wParam, lParam);
                setMessageResult(0);
                //setMessageResult(windowProcedureState.callDefaultWndProc(hWnd, uMsg, wParam, lParam));
                break;

            case User32.WM_SIZING:
                handleSizing(hWnd, uMsg, wParam, lParam);
                // forward the event so that AWT async repaints the window area
                clearMessageResult();
                break;

            case User32.WM_NCHITTEST:
                handleHitTest(hWnd, uMsg, wParam, lParam);
                break;
            case User32.WM_SIZE:
                handleSize();
                if (log.isLoggable(Level.FINEST))
                    log.finest(String.format("WM_SIE size: %d %d", loword(lParam), hiword(lParam)));
                break;
            case User32.WM_MOVE:
                handleMove();
                break;
            case User32.WM_ERASEBKGND:
                handleEraseBackground(hWnd, wParam);
                break;
            case User32.WM_WINDOWPOSCHANGED:
                handleWindowPosChanged();
                break;
            case User32.WM_ACTIVATE:
                handleActivate();
                break;
            case User32.WM_DISPLAYCHANGE:
                handleDisplayChange();
                break;

            case User32.WM_GETMINMAXINFO:
                setMessageResult(windowProcedureState.callClassWndProc(hWnd, uMsg, wParam, lParam));
                handleGetMinMaxInfo(lParam);
                break;
        }
    }

    protected void handleDisplayChange()
    {
        messageHandler.onDisplayChange();
    }

    protected void handleActivate()
    {
        messageHandler.onActivate();
    }

    protected void handleWindowPosChanged()
    {
        messageHandler.onWindowPosChanged();
    }

    protected abstract void handleGetMinMaxInfo(int pMinMaxInfo);

    /**
     * Disables resizing of non-resiable sides. Only the side adjacent to the desktop
     * can be resized, and then only if resizing is {@linkplain WAppBar#isResizable() allowed}.
     * E.g. An appbar aligned to the left of the screen can have it's right side dragged
     * to resize, but no other sides. The parameters are those from the {@code WM_NCHITTEST} message.
     * <p>
     * There appears to be a bug in XP (and possibly earlier) where the {@code DefaultWindowProc}
     * does not correctly handle windows smaller than ca. 20 pixels - clicking on the bottom or right causes
     * the top or left side to be hit.
     *
     * @param hWnd  The native window handle
     * @param uMsg  {@code WM_NCHITTEST}
     * @param wParam
     * @return The hittest code.
     *
     * @see #fixupSmallWindowHitTestBug
     */
    protected void handleHitTest(Pointer hWnd, int uMsg, int wParam, int lParam)
    {
        // first get the "normal" hit area.
        int hit = windowProcedureState.callClassWndProc(hWnd, uMsg, wParam, lParam);
        if (applyHitTestBugfix)
            hit = fixupSmallWindowHitTestBug(hWnd, hit, lParam);
        hit = messageHandler.appBarHitTest(loword(lParam), hiword(lParam), hit);

        boolean resizable = windowProcedureState.getAppBarModel().isResizable();
        if (resizable && !this.restrictResize)
        {
            setMessageResult(hit);
            return;
        }

//        if (log.isLoggable(Level.FINEST))
//            log.finest(String.format("WM_NCHITTEST %d", hit));

        int edge = -1;
        switch (hit)
        {
            case User32.HTBOTTOM:
                edge = ABE_TOP;
                break;
            case User32.HTTOP:
                edge = ABE_BOTTOM;
                break;
            case User32.HTLEFT:
                edge = ABE_RIGHT;
                break;
            case User32.HTRIGHT:
                edge = ABE_LEFT;
                break;
        }
        if (edge!=-1)
            hit = hitEdgeTest(edge, hit);
        else
            hit = hit==User32.HTCAPTION ? hit : User32.HTCLIENT;
        setMessageResult(hit);
    }

    protected void handleEraseBackground(Pointer hWnd, int hDC)
    {

    }

    protected void handleMove()
    {

    }

    protected void handleSize()
    {

    }


    protected void handleEnterSizeOrMove()
    {
        windowProcedureState.setMovingOrSizing(true);
        messageHandler.onSizingOrMovingEnter();
    }


    protected void handleExitSizeOrMove()
    {
        DragGuide dragGuide = windowProcedureState.getDragGuide();

        if (dragGuide.isDragging())
        {
            dragGuide.endDrag(false);
        }
        messageHandler.onSizingOrMovingExit();
        windowProcedureState.setMovingOrSizing(false);
    }

    /**
     * Decodes the minMaxInfo from the message parameter and forwards ti
     * {@link #handleMinMaxInfo(jdesktop.appbar.win32.User32.MinMaxInfo)}
     * @param lParam        A pointer to a {@link User32#}MinMaxInfo} struct.
     */
    protected void getMinMaxInfo(int lParam)
    {
        Pointer p = new Pointer(lParam);

        User32.MinMaxInfo minMaxInfo = new User32.MinMaxInfo(p);
        minMaxInfo.read();
        handleMinMaxInfo(minMaxInfo);
        minMaxInfo.write();
    }

    /**
     * Request from the windows message pump to set the the maximum/minimum sizing dimensions.
     * If the appbar is docked, then sets the max info to at least the maximum size required
     * along the primary edge.
     * @param minMaxInfo
     */
    void handleMinMaxInfo(User32.MinMaxInfo minMaxInfo)
    {
        User32.Point pt = minMaxInfo.ptMinTrackSize;
        // todo - fix this hardcoding. Min width is window size minus client area
        // (so size when client area is 0.)
        pt.x = 8;
        pt.y = 8;

        if (log.isLoggable(Level.FINEST))
            log.finest(String.format("minTrackSize %d,%d", pt.x, pt.y));
    }

    /**
     * Handler for the {@code WM_SIZING} message.
     * @param hWnd
     * @param uMsg      {@code WM_SIZING]
     * @param uParam    The edge that is being sized, one of the WMSZ_xxx constaints.
     * @param lParam    A pointer to a RECT that holds the current sizing bounds
     *  in screen co-ordinates.
     * @return
     */
    protected void handleSizing(Pointer hWnd, int uMsg, int uParam, int lParam)
    {
        if (!handleSizing)
            return;

        Pointer pRect = new Pointer(lParam);

        int left = pRect.getInt(0);
        int top = pRect.getInt(1*4);
        int right = pRect.getInt(2*4);
        int bottom = pRect.getInt(3*4);

        Rectangle r = appBarSizing(uParam, left, top, right, bottom);
        if (r!=null)
        {
            pRect.setInt(0, r.x);
            pRect.setInt(1*4, r.y);
            pRect.setInt(2*4, r.x+r.width);
            pRect.setInt(3*4, r.y+r.height);
        }
    }

    /**
     * Callback to allow inspection/modification of the sizing rectangle.
     * @param wParam    The edge that is being sized, one of the WMSZ_xxx constaints.
     * @param left
     * @param top
     * @param right
     * @param bottom
     * @return The new sizing rectangle, or {@code null} for no change to the sizing bounds.
     */
    protected abstract Rectangle appBarSizing(int wParam, int left, int top, int right, int bottom);

    /**
     * Handles the {@code WM_MOVING} message.
     * @param hWnd
     * @param uMsg
     * @param uParam
     * @param lParam
     * @return
     */
    protected void handleMoving(Pointer hWnd, int uMsg, int uParam, int lParam)
    {
        if (!handleMoving)
            return;

        // todo - push this into the event listener object, so a lot of the drag related properties
        // in appBar move into appBarEvenHandlerSupport
        User32.Point pt = new User32.Point();
        user32.GetCursorPos(pt);
        Point currentPoint = new Point(pt.x, pt.y);

        DragGuide dragGuide = windowProcedureState.getDragGuide();
        assert(dragGuide!=null);
        if (!dragGuide.isDragging())
        {
            dragGuide.init(owner, windowProcedureState.getDesktopEnvironment().getVirtualScreenBounds());
            dragGuide.setDragToFloatBounds(owner.getFloatableDragBounds());
            dragGuide.startDrag(currentPoint);
            assert dragGuide.isDragging();
        }
        Pointer pRect = new Pointer(lParam);

        int left = pRect.getInt(0);
        int top = pRect.getInt(1*4);
        int right = pRect.getInt(2*4);
        int bottom = pRect.getInt(3*4);

        //Rectangle r = notify.appBarMoving(owner, uParam, left, top, right, bottom);
        Rectangle r = new Rectangle(left, top, right-left, bottom-top);
        boolean changed = (dragGuide.updateDrag(currentPoint, r));
        if (log.isLoggable(Level.FINEST))
            log.log(Level.FINEST, String.format("Drag update %b, rect %d %d %d %d",
                    changed, r.x, r.y, r.width, r.height));
        if (changed || windowProcedureState.getAppBarModel().isDocked())
        {
            pRect.setInt(0, r.x);
            pRect.setInt(1*4, r.y);
            pRect.setInt(2*4, r.x+r.width);
            pRect.setInt(3*4, r.y+r.height);
        }
    }

    protected int hiword(int lParam)
    {
        return lParam>>>16;
    }

    protected int loword(int lParam)
    {
        return lParam&0xFFFF;
    }

    /**
     * Windows Bug: when the window is less than ca. 20 px,
     * the default window proc returns {@code HTLEFT} when the right sizing border
     * is moused over, and {@code HTTOP} when the bottom is moused over.
     * @return
     */
    private int fixupSmallWindowHitTestBug(Pointer hWnd, int hit, int lParam)
    {
        if (hit==User32.HTTOP || hit==User32.HTLEFT )
        {
            if (windowProcedureState.getAppBarModel().getBarSize()<30)
            {
                int x = loword(lParam);
                int y = hiword(lParam);
                // get the bottom right of the client rect
                user32.GetClientRect(hWnd, scratchRect);
                scratchPoint.x = scratchRect.right;
                scratchPoint.y = scratchRect.bottom;
                user32.ClientToScreen(hWnd, scratchPoint);

                // the point is further right/down than the client rect, so it must
                // be in the non client rect.
                if (hit==User32.HTLEFT && x>=scratchPoint.x)
                    hit = User32.HTRIGHT;
                if (hit==User32.HTTOP && y>=scratchPoint.y)
                    hit = User32.HTBOTTOM;
            }
        }
        return hit;
    }

    protected int hitEdgeTest(int edge, int nchit)
    {
        return (WAppBar.edgeCode(windowProcedureState.getAppBarModel().getEdge()) == edge) ? nchit : User32.HTCLIENT;
    }
}
